/*-------------------------------------------------------------------------
 *
 * slru.c
 *		用于事务状态日志文件的简单 LRU 缓冲
 *
 * 我们使用简单的最近最少使用方案来管理一组页面缓冲。在普通情况下，
 * 我们预计写入流量主要发生在最新页面（以及在页面过渡之后不久的
 * 先前页面）。读取流量可能涉及更大范围的页面，但在任何情况下，
 * 相对较少的页面缓冲应该足够。因此，我们只是使用简单线性搜索来
 * 搜索缓冲；没有必要使用哈希表或其他复杂的东西。
 * 管理算法是简单的 LRU，除非我们永远不会交换出最新页面（因为
 * 我们知道它最终还会被访问）。
 *
 * 我们使用一个控制 LWLock 来保护共享数据结构，以及用于每个缓冲
 * 的 LWLocks 来同步每个缓冲的 I/O。必须持有控制锁才能检查或修改
 * 任何共享状态。正在读取或写入页面缓冲的进程不会持有控制锁，
 * 仅持有正在处理的缓冲的每个缓冲锁。
 *
 * “持有控制锁”在所有情况下都意味着独占锁，除了 SimpleLruReadPage_ReadOnly() ；请见
 * SlruRecentlyUsed() 的注释以了解其影响。
 *
 * 在缓冲上启动 I/O 时，我们在释放控制锁之前独占获取每个缓冲锁。
 * 每个缓冲锁在完成 I/O 后释放，重新获得控制锁，并更新共享状态。
 * （此处不会发生死锁，因为我们从不尝试在某人已经在同一缓冲上执行 I/O 时
 * 启动 I/O。）
 * 要等待 I/O 完成，请释放控制锁，以共享模式获取每个缓冲锁，立即释放
 * 每个缓冲锁，重新获得控制锁，然后重新检查状态（因为在我们没有锁时可能
 * 发生了任意事情）。
 *
 * 与常规缓冲管理器一样，另一进程有可能重新修改当前正在写入的页面。
 * 通过重新设置页面的 page_dirty 标志来处理此问题。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * src/backend/access/transam/slru.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

#include "access/slru.h"
#include "access/transam.h"
#include "access/xlog.h"
#include "access/xlogutils.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "storage/fd.h"
#include "storage/shmem.h"

#define SlruFileName(ctl, path, seg) \
	snprintf(path, MAXPGPATH, "%s/%04X", (ctl)->Dir, seg)

/*
 * 在SimpleLruWriteAll()期间，我们通常不需要写超过一个或两个物理文件，但我们可能需要每个文件写几个页面。我们可以通过在控制返回到SimpleLruWriteAll()之前保持文件打开来合并I/O请求。这个数据结构记住哪些文件是打开的。
 */
#define MAX_WRITEALL_BUFFERS	16

typedef struct SlruWriteAllData
{
	int			num_files;		/* 实际打开的文件数 */
	int			fd[MAX_WRITEALL_BUFFERS];	/* 他们的文件描述符 */
	int			segno[MAX_WRITEALL_BUFFERS];	/* 他们的日志段号 */
} SlruWriteAllData;

typedef struct SlruWriteAllData *SlruWriteAll;

/*
 * 填充描述段文件的文件标签。我们只使用段号，因为我们可以通过为clog、多事务等分别设置同步处理程序函数来派生我们需要的其他信息。
 */
#define INIT_SLRUFILETAG(a,xx_handler,xx_segno) \
( \
	memset(&(a), 0, sizeof(FileTag)), \
	(a).handler = (xx_handler), \
	(a).segno = (xx_segno) \
)

/*
 * 宏来标记缓冲区槽为“最近最常使用”。注意参数的多次求值！
 *
 * if测试的原因是，通常会对同一页面（特别是最新页面）进行多个连续访问。通过抑制cur_lru_count的无用增量，我们减少了旧页面计数“回绕”的概率，使它们看起来像是最近使用过的。
 *
 * 我们允许此代码被多个进程在SimpleLruReadPage_ReadOnly()内并发执行。只要int的读取和写入是原子的，这就不应该导致任何完全错误的值进入计算。然而，如果一个进程在执行这个宏时被延迟，cur_lru_count或个别page_lru_count条目可能会“重置”为低于它们应该有的值。通过在SlruSelectLRUPage()中的小心处理，这不会造成太大伤害，并且无论如何，最坏的后果只是选择的被驱逐页面不是最优选择。从允许并发读取SLRU页面中获得的收益似乎很值得。
 */
#define SlruRecentlyUsed(shared, slotno)	\
	do { \
		int		fc_new_lru_count = (shared)->cur_lru_count; \
		if (fc_new_lru_count != (shared)->page_lru_count[slotno]) { \
			(shared)->cur_lru_count = ++fc_new_lru_count; \
			(shared)->page_lru_count[slotno] = fc_new_lru_count; \
		} \
	} while (0)

/* SlruReportIOError保存的信息 */
typedef enum
{
	SLRU_OPEN_FAILED,
	SLRU_SEEK_FAILED,
	SLRU_READ_FAILED,
	SLRU_WRITE_FAILED,
	SLRU_FSYNC_FAILED,
	SLRU_CLOSE_FAILED
} SlruErrorCause;

static SlruErrorCause slru_errcause;
static int	slru_errno;


static void fc_SimpleLruZeroLSNs(SlruCtl fc_ctl, int fc_slotno);
static void fc_SimpleLruWaitIO(SlruCtl fc_ctl, int fc_slotno);
static void fc_SlruInternalWritePage(SlruCtl fc_ctl, int fc_slotno, SlruWriteAll fc_fdata);
static bool fc_SlruPhysicalReadPage(SlruCtl fc_ctl, int fc_pageno, int fc_slotno);
static bool fc_SlruPhysicalWritePage(SlruCtl fc_ctl, int fc_pageno, int fc_slotno,
								  SlruWriteAll fc_fdata);
static void fc_SlruReportIOError(SlruCtl fc_ctl, int fc_pageno, TransactionId fc_xid);
static int	fc_SlruSelectLRUPage(SlruCtl fc_ctl, int fc_pageno);

static bool fc_SlruScanDirCbDeleteCutoff(SlruCtl fc_ctl, char *fc_filename,
									  int fc_segpage, void *fc_data);
static void fc_SlruInternalDeleteSegment(SlruCtl fc_ctl, int fc_segno);

/*
 * 共享内存的初始化
 */

Size SimpleLruShmemSize(int fc_nslots, int fc_nlsns)
{
	Size		fc_sz;

	/* 我们假设nslots不会大到导致溢出的风险 */
	fc_sz = MAXALIGN(sizeof(SlruSharedData));
	fc_sz += MAXALIGN(fc_nslots * sizeof(char *));	/* page_buffer[] */
	fc_sz += MAXALIGN(fc_nslots * sizeof(SlruPageStatus));	/* page_status[] */
	fc_sz += MAXALIGN(fc_nslots * sizeof(bool));	/* page_dirty[] */
	fc_sz += MAXALIGN(fc_nslots * sizeof(int));	/* page_number[] */
	fc_sz += MAXALIGN(fc_nslots * sizeof(int));	/* page_lru_count[] */
	fc_sz += MAXALIGN(fc_nslots * sizeof(LWLockPadded));	/* buffer_locks[] */

	if (fc_nlsns > 0)
		fc_sz += MAXALIGN(fc_nslots * fc_nlsns * sizeof(XLogRecPtr));	/* group_lsn[] */

	return BUFFERALIGN(fc_sz) + BLCKSZ * fc_nslots;
}

/*
 * 在共享内存中初始化或附加一个简单的LRU缓存。
 *
 * ctl: 本地（未共享）控制结构的地址。
 * name: SLRU的名称。（这是用户可见的，要慎重选择！）
 * nslots: 要使用的页面槽数量。
 * nlsns: 每个页面的LSN组数量（如果不相关，则设置为零）。
 * ctllock: 用于控制对共享控制结构访问的LWLock。
 * subdir: 相对于PGDATA的子目录，将包含文件。
 * tranche_id: 用于SLRU每个缓冲区LWLocks的LWLock tranche ID。
 * sync_handler: 使用哪一组函数来处理同步请求
 */
void SimpleLruInit(SlruCtl fc_ctl, const char *fc_name, int fc_nslots, int fc_nlsns,
			  LWLock *fc_ctllock, const char *fc_subdir, int fc_tranche_id,
			  SyncRequestHandler fc_sync_handler)
{
	SlruShared	fc_shared;
	bool		fc_found;

	fc_shared = (SlruShared) ShmemInitStruct(fc_name,
										  SimpleLruShmemSize(fc_nslots, fc_nlsns),
										  &fc_found);

	if (!IsUnderPostmaster)
	{
		/* 初始化锁和共享内存区域 */
		char	   *fc_ptr;
		Size		fc_offset;
		int			fc_slotno;

		Assert(!fc_found);

		memset(fc_shared, 0, sizeof(SlruSharedData));

		fc_shared->ControlLock = fc_ctllock;

		fc_shared->num_slots = fc_nslots;
		fc_shared->lsn_groups_per_page = fc_nlsns;

		fc_shared->cur_lru_count = 0;

		/* shared->latest_page_number将在稍后设置 */

		fc_shared->slru_stats_idx = pgstat_get_slru_index(fc_name);

		fc_ptr = (char *) fc_shared;
		fc_offset = MAXALIGN(sizeof(SlruSharedData));
		fc_shared->page_buffer = (char **) (fc_ptr + fc_offset);
		fc_offset += MAXALIGN(fc_nslots * sizeof(char *));
		fc_shared->page_status = (SlruPageStatus *) (fc_ptr + fc_offset);
		fc_offset += MAXALIGN(fc_nslots * sizeof(SlruPageStatus));
		fc_shared->page_dirty = (bool *) (fc_ptr + fc_offset);
		fc_offset += MAXALIGN(fc_nslots * sizeof(bool));
		fc_shared->page_number = (int *) (fc_ptr + fc_offset);
		fc_offset += MAXALIGN(fc_nslots * sizeof(int));
		fc_shared->page_lru_count = (int *) (fc_ptr + fc_offset);
		fc_offset += MAXALIGN(fc_nslots * sizeof(int));

		/* 初始化LWLocks */
		fc_shared->buffer_locks = (LWLockPadded *) (fc_ptr + fc_offset);
		fc_offset += MAXALIGN(fc_nslots * sizeof(LWLockPadded));

		if (fc_nlsns > 0)
		{
			fc_shared->group_lsn = (XLogRecPtr *) (fc_ptr + fc_offset);
			fc_offset += MAXALIGN(fc_nslots * fc_nlsns * sizeof(XLogRecPtr));
		}

		fc_ptr += BUFFERALIGN(fc_offset);
		for (fc_slotno = 0; fc_slotno < fc_nslots; fc_slotno++)
		{
			LWLockInitialize(&fc_shared->buffer_locks[fc_slotno].lock,
							 fc_tranche_id);

			fc_shared->page_buffer[fc_slotno] = fc_ptr;
			fc_shared->page_status[fc_slotno] = SLRU_PAGE_EMPTY;
			fc_shared->page_dirty[fc_slotno] = false;
			fc_shared->page_lru_count[fc_slotno] = 0;
			fc_ptr += BLCKSZ;
		}

		/* 应适应估计的共享内存大小 */
		Assert(fc_ptr - (char *) fc_shared <= SimpleLruShmemSize(fc_nslots, fc_nlsns));
	}
	else
		Assert(fc_found);

	/*
	 * 初始化未共享的控制结构，包括目录路径。我们假设调用者设置了PagePrecedes。
	 */
	fc_ctl->shared = fc_shared;
	fc_ctl->sync_handler = fc_sync_handler;
	strlcpy(fc_ctl->Dir, fc_subdir, sizeof(fc_ctl->Dir));
}

/*
 * 初始化（或重新初始化）一个页面为零。
 *
 * 该页面实际上并不被写入，只是在共享内存中进行设置。
 * 返回新页面的槽号。
 *
 * 进入时必须持有控制锁，退出时也会持有。
 */
int SimpleLruZeroPage(SlruCtl fc_ctl, int fc_pageno)
{
	SlruShared	fc_shared = fc_ctl->shared;
	int			fc_slotno;

	/* 寻找一个合适的缓冲槽来存放页面 */
	fc_slotno = fc_SlruSelectLRUPage(fc_ctl, fc_pageno);
	Assert(fc_shared->page_status[fc_slotno] == SLRU_PAGE_EMPTY ||
		   (fc_shared->page_status[fc_slotno] == SLRU_PAGE_VALID &&
			!fc_shared->page_dirty[fc_slotno]) ||
		   fc_shared->page_number[fc_slotno] == fc_pageno);

	/* 将槽标记为包含此页面 */
	fc_shared->page_number[fc_slotno] = fc_pageno;
	fc_shared->page_status[fc_slotno] = SLRU_PAGE_VALID;
	fc_shared->page_dirty[fc_slotno] = true;
	SlruRecentlyUsed(fc_shared, fc_slotno);

	/* 将缓冲区设置为零 */
	MemSet(fc_shared->page_buffer[fc_slotno], 0, BLCKSZ);

	/* 将此新页面的LSN设置为零 */
	fc_SimpleLruZeroLSNs(fc_ctl, fc_slotno);

	/* 假设此页面现在是最新的活动页面 */
	fc_shared->latest_page_number = fc_pageno;

	/* 更新零页的统计计数器 */
	pgstat_count_slru_page_zeroed(fc_shared->slru_stats_idx);

	return fc_slotno;
}

/*
 * 将我们存储的所有LSN归零对于这个slru页面。
 *
 * 每次创建新页面时，以及每次将页面从磁盘读取到现有缓冲区时都应该调用此函数。
 * （这样的旧页面不能有任何有趣的LSN，因为我们在第一次写入页面之前已经进行了flush。）
 *
 * 这假设InvalidXLogRecPtr全部为0。
 */
static void fc_SimpleLruZeroLSNs(SlruCtl fc_ctl, int fc_slotno)
{
	SlruShared	fc_shared = fc_ctl->shared;

	if (fc_shared->lsn_groups_per_page > 0)
		MemSet(&fc_shared->group_lsn[fc_slotno * fc_shared->lsn_groups_per_page], 0,
			   fc_shared->lsn_groups_per_page * sizeof(XLogRecPtr));
}

/*
 * 等待页面槽上的任何活动I/O完成。（这并不保证在我们返回之前没有启动新的I/O。
 * 实际上，槽可能根本不再包含相同的页面。）
 *
 * 进入时必须持有控制锁，退出时也会持有。
 */
static void fc_SimpleLruWaitIO(SlruCtl fc_ctl, int fc_slotno)
{
	SlruShared	fc_shared = fc_ctl->shared;

	/* 请参见文件开头的说明 */
	LWLockRelease(fc_shared->ControlLock);
	LWLockAcquire(&fc_shared->buffer_locks[fc_slotno].lock, LW_SHARED);
	LWLockRelease(&fc_shared->buffer_locks[fc_slotno].lock);
	LWLockAcquire(fc_shared->ControlLock, LW_EXCLUSIVE);

	/*
	 * 如果槽仍处于I/O进行中的状态，则说明有人已经在该槽上启动了新的I/O，
	 * 或者之前的I/O失败而未能重置页面状态。这种情况确实不应该发生，
	 * 但检查并恢复它似乎值得多花几个周期。我们可以通过查看缓冲锁是否仍然持有来廉价地测试失败（我们假设事务中止会释放锁）。
	 */
	if (fc_shared->page_status[fc_slotno] == SLRU_PAGE_READ_IN_PROGRESS ||
		fc_shared->page_status[fc_slotno] == SLRU_PAGE_WRITE_IN_PROGRESS)
	{
		if (LWLockConditionalAcquire(&fc_shared->buffer_locks[fc_slotno].lock, LW_SHARED))
		{
			/* 确实，I/O必须失败了 */
			if (fc_shared->page_status[fc_slotno] == SLRU_PAGE_READ_IN_PROGRESS)
				fc_shared->page_status[fc_slotno] = SLRU_PAGE_EMPTY;
			else				/* write_in_progress */
			{
				fc_shared->page_status[fc_slotno] = SLRU_PAGE_VALID;
				fc_shared->page_dirty[fc_slotno] = true;
			}
			LWLockRelease(&fc_shared->buffer_locks[fc_slotno].lock);
		}
	}
}

/*
 * 在共享缓冲区中查找一个页面，如果必要则读取它。
 * 页面编号必须对应于一个已经初始化的页面。
 *
 * 如果write_ok为真，则可以返回处于WRITE_IN_PROGRESS状态的页面；
 * 确保对页面的修改是安全的由调用者负责。如果write_ok为假，则
 * 在页面没有正在进行的活动I/O之前不会返回该页面。
 *
 * 传入的xid仅用于错误报告，如果没有与该操作关联的特定xid，则可能是InvalidTransactionId。
 *
 * 返回值是现在持有页面的共享缓冲槽号。
 * 更新缓冲区的LRU访问信息。
 *
 * 进入时必须持有控制锁，退出时也会持有。
 */
int SimpleLruReadPage(SlruCtl fc_ctl, int fc_pageno, bool fc_write_ok,
				  TransactionId fc_xid)
{
	SlruShared	fc_shared = fc_ctl->shared;

	/* 外循环处理重启，如果我们必须等待其他人的I/O */
	for (;;)
	{
		int			fc_slotno;
		bool		fc_ok;

		/* 查看页面是否已在内存中；如果没有，则选择受害者槽 */
		fc_slotno = fc_SlruSelectLRUPage(fc_ctl, fc_pageno);

		/* 我们在内存中找到了页面吗？ */
		if (fc_shared->page_number[fc_slotno] == fc_pageno &&
			fc_shared->page_status[fc_slotno] != SLRU_PAGE_EMPTY)
		{
			/*
			 * 如果页面仍在读取中，必须等待I/O。同样，
			 * 如果页面正在写入且调用者表示这样不行。
			 */
			if (fc_shared->page_status[fc_slotno] == SLRU_PAGE_READ_IN_PROGRESS ||
				(fc_shared->page_status[fc_slotno] == SLRU_PAGE_WRITE_IN_PROGRESS &&
				 !fc_write_ok))
			{
				fc_SimpleLruWaitIO(fc_ctl, fc_slotno);
				/* 现在我们必须从顶部重新检查状态 */
				continue;
			}
			/* 否则，它可以使用 */
			SlruRecentlyUsed(fc_shared, fc_slotno);

			/* 更新SLRU中找到的页面的统计计数器 */
			pgstat_count_slru_page_hit(fc_shared->slru_stats_idx);

			return fc_slotno;
		}

		/* 我们没有找到匹配项；断言我们选择了一个可释放的槽 */
		Assert(fc_shared->page_status[fc_slotno] == SLRU_PAGE_EMPTY ||
			   (fc_shared->page_status[fc_slotno] == SLRU_PAGE_VALID &&
				!fc_shared->page_dirty[fc_slotno]));

		/* 标记槽为读取忙 */
		fc_shared->page_number[fc_slotno] = fc_pageno;
		fc_shared->page_status[fc_slotno] = SLRU_PAGE_READ_IN_PROGRESS;
		fc_shared->page_dirty[fc_slotno] = false;

		/* 获取每个缓冲区的锁（不会死锁，见顶部说明） */
		LWLockAcquire(&fc_shared->buffer_locks[fc_slotno].lock, LW_EXCLUSIVE);

		/* 在进行I/O时释放控制锁 */
		LWLockRelease(fc_shared->ControlLock);

		/* 执行读取 */
		fc_ok = fc_SlruPhysicalReadPage(fc_ctl, fc_pageno, fc_slotno);

		/* 将此新读取页面的LSN设置为零 */
		fc_SimpleLruZeroLSNs(fc_ctl, fc_slotno);

		/* 重新获取控制锁并更新页面状态 */
		LWLockAcquire(fc_shared->ControlLock, LW_EXCLUSIVE);

		Assert(fc_shared->page_number[fc_slotno] == fc_pageno &&
			   fc_shared->page_status[fc_slotno] == SLRU_PAGE_READ_IN_PROGRESS &&
			   !fc_shared->page_dirty[fc_slotno]);

		fc_shared->page_status[fc_slotno] = fc_ok ? SLRU_PAGE_VALID : SLRU_PAGE_EMPTY;

		LWLockRelease(&fc_shared->buffer_locks[fc_slotno].lock);

		/* 如果我们失败了，现在可以报告错误 */
		if (!fc_ok)
			fc_SlruReportIOError(fc_ctl, fc_pageno, fc_xid);

		SlruRecentlyUsed(fc_shared, fc_slotno);

		/* 更新SLRU中未找到的页面的统计计数器 */
		pgstat_count_slru_page_read(fc_shared->slru_stats_idx);

		return fc_slotno;
	}
}

/*
 * 在共享缓冲区中找到一个页面，如有必要则读取它。
 * 页面编号必须对应于一个已经初始化的页面。
 * 调用者必须仅打算以只读方式访问该页面。
 *
 * 传入的xid仅用于错误报告，如果此操作没有关联的特定xid，
 * 则可能为InvalidTransactionId。
 *
 * 返回值是现在持有该页面的共享缓冲区槽号。
 * 缓冲区的LRU访问信息已更新。
 *
 * 入口时不得持有控制锁，但在退出时将持有。
 * 锁是共享锁还是独占锁未指定。
 */
int SimpleLruReadPage_ReadOnly(SlruCtl fc_ctl, int fc_pageno, TransactionId fc_xid)
{
	SlruShared	fc_shared = fc_ctl->shared;
	int			fc_slotno;

	/* 尝试在仅持有共享锁的情况下找到页面 */
	LWLockAcquire(fc_shared->ControlLock, LW_SHARED);

	/* 查看页面是否已经在缓冲区中 */
	for (fc_slotno = 0; fc_slotno < fc_shared->num_slots; fc_slotno++)
	{
		if (fc_shared->page_number[fc_slotno] == fc_pageno &&
			fc_shared->page_status[fc_slotno] != SLRU_PAGE_EMPTY &&
			fc_shared->page_status[fc_slotno] != SLRU_PAGE_READ_IN_PROGRESS)
		{
			/* 查看SlruRecentlyUsed宏的注释 */
			SlruRecentlyUsed(fc_shared, fc_slotno);

			/* 更新SLRU中找到的页面的统计计数器 */
			pgstat_count_slru_page_hit(fc_shared->slru_stats_idx);

			return fc_slotno;
		}
	}

	/* 没有运气，因此切换到正常的独占锁并进行常规读取 */
	LWLockRelease(fc_shared->ControlLock);
	LWLockAcquire(fc_shared->ControlLock, LW_EXCLUSIVE);

	return SimpleLruReadPage(fc_ctl, fc_pageno, true, fc_xid);
}

/*
 * 从共享缓冲区写入一个页面（如有必要）。
 * 如果指定的槽未被标记为脏，则不执行任何操作。
 *
 * 注意：这里只尝试一次写入。因此，在退出时页面
 * 仍可能是脏的（如果其他人在写入期间重新标记为脏）。
 * 但是，即使页面已经在写入，我们也会尝试新的写入；
 * 这用于检查点。
 *
 * 入口时必须持有控制锁，退出时将持有。
 */
static void fc_SlruInternalWritePage(SlruCtl fc_ctl, int fc_slotno, SlruWriteAll fc_fdata)
{
	SlruShared	fc_shared = fc_ctl->shared;
	int			fc_pageno = fc_shared->page_number[fc_slotno];
	bool		fc_ok;

	/* 如果写入正在进行中，请等待其完成 */
	while (fc_shared->page_status[fc_slotno] == SLRU_PAGE_WRITE_IN_PROGRESS &&
		   fc_shared->page_number[fc_slotno] == fc_pageno)
	{
		fc_SimpleLruWaitIO(fc_ctl, fc_slotno);
	}

	/*
	 * 如果页面未被标记为脏，或者缓冲区不再包含
	 * 我们被调用时所请求的相同页面，则不执行任何操作。
	 */
	if (!fc_shared->page_dirty[fc_slotno] ||
		fc_shared->page_status[fc_slotno] != SLRU_PAGE_VALID ||
		fc_shared->page_number[fc_slotno] != fc_pageno)
		return;

	/*
	 * 标记槽为写入忙，并清除脏位。此时，如果该页面的
	 * 事务状态更新将再次标记为脏。
	 */
	fc_shared->page_status[fc_slotno] = SLRU_PAGE_WRITE_IN_PROGRESS;
	fc_shared->page_dirty[fc_slotno] = false;

	/* 获取每个缓冲区的锁（不会死锁，见顶部说明） */
	LWLockAcquire(&fc_shared->buffer_locks[fc_slotno].lock, LW_EXCLUSIVE);

	/* 在进行I/O时释放控制锁 */
	LWLockRelease(fc_shared->ControlLock);

	/* 执行写入 */
	fc_ok = fc_SlruPhysicalWritePage(fc_ctl, fc_pageno, fc_slotno, fc_fdata);

	/* 如果失败，并且我们在刷新中，最好关闭文件 */
	if (!fc_ok && fc_fdata)
	{
		int			fc_i;

		for (fc_i = 0; fc_i < fc_fdata->num_files; fc_i++)
			CloseTransientFile(fc_fdata->fd[fc_i]);
	}

	/* 重新获取控制锁并更新页面状态 */
	LWLockAcquire(fc_shared->ControlLock, LW_EXCLUSIVE);

	Assert(fc_shared->page_number[fc_slotno] == fc_pageno &&
		   fc_shared->page_status[fc_slotno] == SLRU_PAGE_WRITE_IN_PROGRESS);

	/* 如果我们未能写入，请再次将页面标记为脏 */
	if (!fc_ok)
		fc_shared->page_dirty[fc_slotno] = true;

	fc_shared->page_status[fc_slotno] = SLRU_PAGE_VALID;

	LWLockRelease(&fc_shared->buffer_locks[fc_slotno].lock);

	/* 如果我们失败了，现在可以报告错误 */
	if (!fc_ok)
		fc_SlruReportIOError(fc_ctl, fc_pageno, InvalidTransactionId);

	/* 如果是检查点的一部分，将其计算为已写入的缓冲区。 */
	if (fc_fdata)
		CheckpointStats.ckpt_bufs_written++;
}

/*
 * SlruInternalWritePage的包装，供外部调用者使用。
 * fdata在这里始终传入NULL。
 */
void SimpleLruWritePage(SlruCtl fc_ctl, int fc_slotno)
{
	fc_SlruInternalWritePage(fc_ctl, fc_slotno, NULL);
}

/*
 * 返回给定页面在磁盘上是否存在。
 *
 * 返回false意味着文件不存在，或者文件的大小不足以
 * 包含给定页面。
 */
bool SimpleLruDoesPhysicalPageExist(SlruCtl fc_ctl, int fc_pageno)
{
	int			fc_segno = fc_pageno / SLRU_PAGES_PER_SEGMENT;
	int			fc_rpageno = fc_pageno % SLRU_PAGES_PER_SEGMENT;
	int			fc_offset = fc_rpageno * BLCKSZ;
	char		fc_path[MAXPGPATH];
	int			fc_fd;
	bool		fc_result;
	off_t		fc_endpos;

	/* 更新已检查页面的统计计数器 */
	pgstat_count_slru_page_exists(fc_ctl->shared->slru_stats_idx);

	SlruFileName(fc_ctl, fc_path, fc_segno);

	fc_fd = OpenTransientFile(fc_path, O_RDONLY | PG_BINARY);
	if (fc_fd < 0)
	{
		/* 预期：文件不存在 */
		if (errno == ENOENT)
			return false;

		/* 正常报告错误 */
		slru_errcause = SLRU_OPEN_FAILED;
		slru_errno = errno;
		fc_SlruReportIOError(fc_ctl, fc_pageno, 0);
	}

	if ((fc_endpos = lseek(fc_fd, 0, SEEK_END)) < 0)
	{
		slru_errcause = SLRU_SEEK_FAILED;
		slru_errno = errno;
		fc_SlruReportIOError(fc_ctl, fc_pageno, 0);
	}

	fc_result = fc_endpos >= (off_t) (fc_offset + BLCKSZ);

	if (CloseTransientFile(fc_fd) != 0)
	{
		slru_errcause = SLRU_CLOSE_FAILED;
		slru_errno = errno;
		return false;
	}

	return fc_result;
}

/*
 * 将（之前存在的）页面物理读取到缓冲槽
 *
 * 如果失败，我们不能仅仅调用 ereport(ERROR)，因为调用者已在
 * 共享内存中放置了必须撤销的状态。因此，我们返回 false，并在
 * 静态变量中保存足够的信息，以便 SlruReportIOError 可以进行报告。
 *
 * 目前，假设在读/写操作之间保持文件指针打开并不值得。我们可以缓存一个虚拟文件指针……
 */
static bool fc_SlruPhysicalReadPage(SlruCtl fc_ctl, int fc_pageno, int fc_slotno)
{
	SlruShared	fc_shared = fc_ctl->shared;
	int			fc_segno = fc_pageno / SLRU_PAGES_PER_SEGMENT;
	int			fc_rpageno = fc_pageno % SLRU_PAGES_PER_SEGMENT;
	off_t		fc_offset = fc_rpageno * BLCKSZ;
	char		fc_path[MAXPGPATH];
	int			fc_fd;

	SlruFileName(fc_ctl, fc_path, fc_segno);

	/*
	 * 在崩溃和重启的情况下，我们可能会收到命令来设置事务的提交状态，
	 * 这些事务的位在已截断的提交日志段中（见 SlruPhysicalWritePage 的注释）。
	 * 因此，如果我们处于恢复状态，则允许文件不存在的情况，并返回零。
	 */
	fc_fd = OpenTransientFile(fc_path, O_RDONLY | PG_BINARY);
	if (fc_fd < 0)
	{
		if (errno != ENOENT || !InRecovery)
		{
			slru_errcause = SLRU_OPEN_FAILED;
			slru_errno = errno;
			return false;
		}

		ereport(LOG,
				(errmsg("file \"%s\" doesn't exist, reading as zeroes",
						fc_path)));
		MemSet(fc_shared->page_buffer[fc_slotno], 0, BLCKSZ);
		return true;
	}

	errno = 0;
	pgstat_report_wait_start(WAIT_EVENT_SLRU_READ);
	if (pg_pread(fc_fd, fc_shared->page_buffer[fc_slotno], BLCKSZ, fc_offset) != BLCKSZ)
	{
		pgstat_report_wait_end();
		slru_errcause = SLRU_READ_FAILED;
		slru_errno = errno;
		CloseTransientFile(fc_fd);
		return false;
	}
	pgstat_report_wait_end();

	if (CloseTransientFile(fc_fd) != 0)
	{
		slru_errcause = SLRU_CLOSE_FAILED;
		slru_errno = errno;
		return false;
	}

	return true;
}

/*
 * 从缓冲槽物理写入页面
 *
 * 如果失败，我们不能仅仅调用 ereport(ERROR)，因为调用者已在
 * 共享内存中放置了必须撤销的状态。因此，我们返回 false，并在
 * 静态变量中保存足够的信息，以便 SlruReportIOError 可以进行报告。
 *
 * 目前，假设在独立的读/写操作之间保持文件指针打开并不值得。不过，
 * 在 SimpleLruWriteAll 期间我们会进行批量操作。
 *
 * fdata 在独立写入时为 NULL，在 SimpleLruWriteAll 时指向打开文件的信息。
 */
static bool fc_SlruPhysicalWritePage(SlruCtl fc_ctl, int fc_pageno, int fc_slotno, SlruWriteAll fc_fdata)
{
	SlruShared	fc_shared = fc_ctl->shared;
	int			fc_segno = fc_pageno / SLRU_PAGES_PER_SEGMENT;
	int			fc_rpageno = fc_pageno % SLRU_PAGES_PER_SEGMENT;
	off_t		fc_offset = fc_rpageno * BLCKSZ;
	char		fc_path[MAXPGPATH];
	int			fc_fd = -1;

	/* 更新已写入页面的统计计数器 */
	pgstat_count_slru_page_written(fc_shared->slru_stats_idx);

	/*
	 * 如果合适，则遵循先写 WAL 后写数据的规则，以便我们不
	 * 会在相关的 WAL 记录之前写出数据。这与主缓冲管理器中的
	 * FlushBuffer() 中执行的操作相同。
	 */
	if (fc_shared->group_lsn != NULL)
	{
		/*
		 * 我们必须确定页面的较大异步提交 LSN。这有点繁琐，
		 * 但由于这个函数本身就是一个慢路径，所以在这里执行
		 * 这个操作似乎比维护每页的 LSN 变量要好（这在
		 * 事务提交路径中需要额外的比较）。
		 */
		XLogRecPtr	fc_max_lsn;
		int			fc_lsnindex,
					fc_lsnoff;

		fc_lsnindex = fc_slotno * fc_shared->lsn_groups_per_page;
		fc_max_lsn = fc_shared->group_lsn[fc_lsnindex++];
		for (fc_lsnoff = 1; fc_lsnoff < fc_shared->lsn_groups_per_page; fc_lsnoff++)
		{
			XLogRecPtr	fc_this_lsn = fc_shared->group_lsn[fc_lsnindex++];

			if (fc_max_lsn < fc_this_lsn)
				fc_max_lsn = fc_this_lsn;
		}

		if (!XLogRecPtrIsInvalid(fc_max_lsn))
		{
			/*
			 * 如上所述，这里不接受 elog(ERROR)，所以如果
			 * XLogFlush 失败，我们必须 PANIC。这并不是太多
			 * 的限制，因为 XLogFlush 基本上在所有关键
			 * 部分中，但让我们确保。
			 */
			START_CRIT_SECTION();
			XLogFlush(fc_max_lsn);
			END_CRIT_SECTION();
		}
	}

	/*
	 * 在 WriteAll 期间，我们可能已经打开了所需的文件。
	 */
	if (fc_fdata)
	{
		int			fc_i;

		for (fc_i = 0; fc_i < fc_fdata->num_files; fc_i++)
		{
			if (fc_fdata->segno[fc_i] == fc_segno)
			{
				fc_fd = fc_fdata->fd[fc_i];
				break;
			}
		}
	}

	if (fc_fd < 0)
	{
		/*
		 * 如果文件尚不存在，我们应该创建它。 
		 * 当写入不是其段中第一个页面的页面时，可能需要这样做；我们假设操作系统能够处理这一情况。 
		 * （注意：看起来似乎只有在调用SimpleLruZeroPage以获取段的第一页时创建文件是没问题的。 
		 * 然而，如果在崩溃和重启后，REDO逻辑选择从最新检查点之前的检查点重放日志，那么
		 * 可能会收到命令来设置已经从提交日志中截断的事务的事务状态。 
		 * 处理这件事最简单的方法是在这里和SlruPhysicalReadPage中接受对不存在文件的引用。）
		 *
		 * 注意：可能会有多个后端同时为同一文件的不同页面执行此代码。因此，
		 * 不要使用O_EXCL或O_TRUNC或类似的东西。
		 */
		SlruFileName(fc_ctl, fc_path, fc_segno);
		fc_fd = OpenTransientFile(fc_path, O_RDWR | O_CREAT | PG_BINARY);
		if (fc_fd < 0)
		{
			slru_errcause = SLRU_OPEN_FAILED;
			slru_errno = errno;
			return false;
		}

		if (fc_fdata)
		{
			if (fc_fdata->num_files < MAX_WRITEALL_BUFFERS)
			{
				fc_fdata->fd[fc_fdata->num_files] = fc_fd;
				fc_fdata->segno[fc_fdata->num_files] = fc_segno;
				fc_fdata->num_files++;
			}
			else
			{
				/*
				 * 在不太可能的情况下我们超过MAX_FLUSH_BUFFERS， 
				 * 回退到将其视为独立写入。
				 */
				fc_fdata = NULL;
			}
		}
	}

	errno = 0;
	pgstat_report_wait_start(WAIT_EVENT_SLRU_WRITE);
	if (pg_pwrite(fc_fd, fc_shared->page_buffer[fc_slotno], BLCKSZ, fc_offset) != BLCKSZ)
	{
		pgstat_report_wait_end();
		/* 如果写入未设置 errno，假设问题是没有磁盘空间 */
		if (errno == 0)
			errno = ENOSPC;
		slru_errcause = SLRU_WRITE_FAILED;
		slru_errno = errno;
		if (!fc_fdata)
			CloseTransientFile(fc_fd);
		return false;
	}
	pgstat_report_wait_end();

	/* 为检查点请求排队同步请求。 */
	if (fc_ctl->sync_handler != SYNC_HANDLER_NONE)
	{
		FileTag		fc_tag;

		INIT_SLRUFILETAG(fc_tag, fc_ctl->sync_handler, fc_segno);
		if (!RegisterSyncRequest(&fc_tag, SYNC_REQUEST, false))
		{
			/* 没有空间排队同步请求。同步处理。 */
			pgstat_report_wait_start(WAIT_EVENT_SLRU_SYNC);
			if (pg_fsync(fc_fd) != 0)
			{
				pgstat_report_wait_end();
				slru_errcause = SLRU_FSYNC_FAILED;
				slru_errno = errno;
				CloseTransientFile(fc_fd);
				return false;
			}
			pgstat_report_wait_end();
		}
	}

	/* 关闭文件，除非是刷新请求的一部分。 */
	if (!fc_fdata)
	{
		if (CloseTransientFile(fc_fd) != 0)
		{
			slru_errcause = SLRU_CLOSE_FAILED;
			slru_errno = errno;
			return false;
		}
	}

	return true;
}

/*
 * 在SlruPhysicalReadPage或SlruPhysicalWritePage失败后发出错误信息。 
 * 在清理共享内存状态后调用此函数。
 */
static void fc_SlruReportIOError(SlruCtl fc_ctl, int fc_pageno, TransactionId fc_xid)
{
	int			fc_segno = fc_pageno / SLRU_PAGES_PER_SEGMENT;
	int			fc_rpageno = fc_pageno % SLRU_PAGES_PER_SEGMENT;
	int			fc_offset = fc_rpageno * BLCKSZ;
	char		fc_path[MAXPGPATH];

	SlruFileName(fc_ctl, fc_path, fc_segno);
	errno = slru_errno;
	switch (slru_errcause)
	{
		case SLRU_OPEN_FAILED:
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not access status of transaction %u", fc_xid),
					 errdetail("Could not open file \"%s\": %m.", fc_path)));
			break;
		case SLRU_SEEK_FAILED:
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not access status of transaction %u", fc_xid),
					 errdetail("Could not seek in file \"%s\" to offset %d: %m.",
							   fc_path, fc_offset)));
			break;
		case SLRU_READ_FAILED:
			if (errno)
				ereport(ERROR,
						(errcode_for_file_access(),
						 errmsg("could not access status of transaction %u", fc_xid),
						 errdetail("Could not read from file \"%s\" at offset %d: %m.",
								   fc_path, fc_offset)));
			else
				ereport(ERROR,
						(errmsg("could not access status of transaction %u", fc_xid),
						 errdetail("Could not read from file \"%s\" at offset %d: read too few bytes.", fc_path, fc_offset)));
			break;
		case SLRU_WRITE_FAILED:
			if (errno)
				ereport(ERROR,
						(errcode_for_file_access(),
						 errmsg("could not access status of transaction %u", fc_xid),
						 errdetail("Could not write to file \"%s\" at offset %d: %m.",
								   fc_path, fc_offset)));
			else
				ereport(ERROR,
						(errmsg("could not access status of transaction %u", fc_xid),
						 errdetail("Could not write to file \"%s\" at offset %d: wrote too few bytes.",
								   fc_path, fc_offset)));
			break;
		case SLRU_FSYNC_FAILED:
			ereport(data_sync_elevel(ERROR),
					(errcode_for_file_access(),
					 errmsg("could not access status of transaction %u", fc_xid),
					 errdetail("Could not fsync file \"%s\": %m.",
							   fc_path)));
			break;
		case SLRU_CLOSE_FAILED:
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not access status of transaction %u", fc_xid),
					 errdetail("Could not close file \"%s\": %m.",
							   fc_path)));
			break;
		default:
			/* 不能到这里，我们信任 */
			elog(ERROR, "unrecognized SimpleLru error cause: %d",
				 (int) slru_errcause);
			break;
	}
}

/*
 * 在需要一个空闲槽位时选择重用的槽位。
 *
 * 目标页码被传递，因为我们需要考虑到在我们进行I/O以释放槽位时可能有其他进程读取目标页面。 
 * 因此，检查或重新检查查看是否有槽位已经持有目标页面，如果有则返回该槽位。 
 * 因此，返回的槽位*要么*是已经持有pageno的槽位（可以是EMPTY以外的任何状态），
 * *要么*是可释放的槽位（状态为EMPTY或CLEAN）。
 *
 * 进入时必须持有控制锁，退出时将保持控制锁。
 */
static int fc_SlruSelectLRUPage(SlruCtl fc_ctl, int fc_pageno)
{
	SlruShared	fc_shared = fc_ctl->shared;

	/* 外层循环在I/O后处理重启 */
	for (;;)
	{
		int			fc_slotno;
		int			fc_cur_count;
		int			fc_bestvalidslot = 0;	/* 保持编译器安静 */
		int			fc_best_valid_delta = -1;
		int			fc_best_valid_page_number = 0; /* 保持编译器安静 */
		int			fc_bestinvalidslot = 0;	/* 保持编译器安静 */
		int			fc_best_invalid_delta = -1;
		int			fc_best_invalid_page_number = 0;	/* 保持编译器安静 */

		/* 查看页面是否已经分配了缓冲区 */
		for (fc_slotno = 0; fc_slotno < fc_shared->num_slots; fc_slotno++)
		{
			if (fc_shared->page_number[fc_slotno] == fc_pageno &&
				fc_shared->page_status[fc_slotno] != SLRU_PAGE_EMPTY)
				return fc_slotno;
		}

		/*
		 * 如果我们找到任何空槽，就选择那个。否则选择一个
		 * 牺牲页面进行替换。我们通常选择最少使用的有效页面，
		 * 但我们绝不会选择包含 latest_page_number 的槽，
		 * 即使它看起来是最少使用的。我们
		 * 将选择一个已经在 I/O 忙的槽，仅在没有
		 * 其他选择时：一旦读取完成，读取忙的槽就不再是
		 * 最少使用的，而等待写忙槽的 I/O 要低于
		 * 选择其他槽。测试表明，我们选择的槽
		 * 通常会是干净的，从而使我们能够立即开始读取。
		 *
		 * 通常，page_lru_count 值都是不同的，因此
		 * 将有一个明确的 LRU 页面。但由于我们允许
		 * 在 SimpleLruReadPage_ReadOnly() 中同时执行
		 * SlruRecentlyUsed()，因此可能有多个页面
		 * 获得相同的 lru_count 值。在这种情况下，我们通过
		 * 选择最久远的页面来打破平局。
		 *
		 * 请注意，这一行强制将 cur_lru_count 提高到一个
		 * 确定超出循环结束后 page_lru_count 数组中的
		 * 任何值的值。这确保了下一次执行 SlruRecentlyUsed
		 * 将标记页面为新使用，即使它是当前计数值的页面。
		 * 这使我们回到在多个页面具有相同 lru_count 时
		 * 拥有良好数据的道路上。
		 */
		fc_cur_count = (fc_shared->cur_lru_count)++;
		for (fc_slotno = 0; fc_slotno < fc_shared->num_slots; fc_slotno++)
		{
			int			fc_this_delta;
			int			fc_this_page_number;

			if (fc_shared->page_status[fc_slotno] == SLRU_PAGE_EMPTY)
				return fc_slotno;
			fc_this_delta = fc_cur_count - fc_shared->page_lru_count[fc_slotno];
			if (fc_this_delta < 0)
			{
				/*
				 * 清理，以防共享更新导致 cur_count
				 * 增量“丢失”。我们回退页面计数，
				 * 而不是尝试增加 cur_count，以避免任何
				 * 无限循环或在计数回绕情况下失败的问题。
				 */
				fc_shared->page_lru_count[fc_slotno] = fc_cur_count;
				fc_this_delta = 0;
			}
			fc_this_page_number = fc_shared->page_number[fc_slotno];
			if (fc_this_page_number == fc_shared->latest_page_number)
				continue;
			if (fc_shared->page_status[fc_slotno] == SLRU_PAGE_VALID)
			{
				if (fc_this_delta > fc_best_valid_delta ||
					(fc_this_delta == fc_best_valid_delta &&
					 fc_ctl->PagePrecedes(fc_this_page_number,
									   fc_best_valid_page_number)))
				{
					fc_bestvalidslot = fc_slotno;
					fc_best_valid_delta = fc_this_delta;
					fc_best_valid_page_number = fc_this_page_number;
				}
			}
			else
			{
				if (fc_this_delta > fc_best_invalid_delta ||
					(fc_this_delta == fc_best_invalid_delta &&
					 fc_ctl->PagePrecedes(fc_this_page_number,
									   fc_best_invalid_page_number)))
				{
					fc_bestinvalidslot = fc_slotno;
					fc_best_invalid_delta = fc_this_delta;
					fc_best_invalid_page_number = fc_this_page_number;
				}
			}
		}

		/*
		 * 如果所有页面（除了可能是最新的页面）都是 I/O 忙的，
		 * 我们将必须等待 I/O 完成，然后重试。在这种不幸的情况下，
		 * 我们选择等待最少使用槽的 I/O，假设它可能是所有
		 * 进行中的 I/O 中最早发起的，因此可能先完成。
		 */
		if (fc_best_valid_delta < 0)
		{
			fc_SimpleLruWaitIO(fc_ctl, fc_bestinvalidslot);
			continue;
		}

		/*
		 * 如果选择的页面是干净的，我们就可以了。
		 */
		if (!fc_shared->page_dirty[fc_bestvalidslot])
			return fc_bestvalidslot;

		/*
		 * 写入页面。
		 */
		fc_SlruInternalWritePage(fc_ctl, fc_bestvalidslot, NULL);

		/*
		 * 现在返回循环并重试。这是处理
		 * 角落情况下最简单的方法，例如牺牲页面在我们
		 * 写入时被重新弄脏。
		 */
	}
}

/*
 * 在检查点或数据库关闭期间将脏页面写入磁盘。刷写
 * 将推迟到下一个调用 ProcessSyncRequests()，尽管我们在此
 * 将目录同步，以确保新创建的目录
 * 条目在磁盘上。
 */
void SimpleLruWriteAll(SlruCtl fc_ctl, bool fc_allow_redirtied)
{
	SlruShared	fc_shared = fc_ctl->shared;
	SlruWriteAllData fc_fdata;
	int			fc_slotno;
	int			fc_pageno = 0;
	int			fc_i;
	bool		fc_ok;

	/* 更新刷新的统计计数器 */
	pgstat_count_slru_flush(fc_shared->slru_stats_idx);

	/*
	 * 查找并写入脏页面
	 */
	fc_fdata.num_files = 0;

	LWLockAcquire(fc_shared->ControlLock, LW_EXCLUSIVE);

	for (fc_slotno = 0; fc_slotno < fc_shared->num_slots; fc_slotno++)
	{
		fc_SlruInternalWritePage(fc_ctl, fc_slotno, &fc_fdata);

		/*
		 * 在某些地方（如检查点），我们无法断言槽
		 * 现在是干净的，因为其他进程可能已经重新弄脏了它。
		 * 这没关系。
		 */
		Assert(fc_allow_redirtied ||
			   fc_shared->page_status[fc_slotno] == SLRU_PAGE_EMPTY ||
			   (fc_shared->page_status[fc_slotno] == SLRU_PAGE_VALID &&
				!fc_shared->page_dirty[fc_slotno]));
	}

	LWLockRelease(fc_shared->ControlLock);

	/*
	 * 现在关闭任何打开的文件
	 */
	fc_ok = true;
	for (fc_i = 0; fc_i < fc_fdata.num_files; fc_i++)
	{
		if (CloseTransientFile(fc_fdata.fd[fc_i]) != 0)
		{
			slru_errcause = SLRU_CLOSE_FAILED;
			slru_errno = errno;
			fc_pageno = fc_fdata.segno[fc_i] * SLRU_PAGES_PER_SEGMENT;
			fc_ok = false;
		}
	}
	if (!fc_ok)
		fc_SlruReportIOError(fc_ctl, fc_pageno, InvalidTransactionId);

	/* 确保新文件的目录条目已写入磁盘。 */
	if (fc_ctl->sync_handler != SYNC_HANDLER_NONE)
		fsync_fname(fc_ctl->Dir, true);
}

/*
 * 删除所有在传入页面编号之前的段
 *
 * 所有 SLRU 都通过 LWLock 或仅在检查点期间调用此函数，防止并发调用。互斥必须在计算 cutoffPage 之前开始。互斥必须在进行任何限制更新后结束，这将允许其他后端在包含 cutoffPage 的段之前写入新数据。否则，当 SLRU 颇为满时，SimpleLruTruncate() 可能会在刚写入新数据后删除该段。
 */
void SimpleLruTruncate(SlruCtl fc_ctl, int fc_cutoffPage)
{
	SlruShared	fc_shared = fc_ctl->shared;
	int			fc_slotno;

	/* 更新截断的统计计数器 */
	pgstat_count_slru_truncate(fc_shared->slru_stats_idx);

	/*
	 * 扫描共享内存并删除截止页面之前的任何页面，以
	 * 确保我们将来不会重写它们。 （因为这通常在
	 * 检查点期间或刚后调用，任何脏页面应该已经被刷新
	 * ……我们只是多加小心。）
	 */
	LWLockAcquire(fc_shared->ControlLock, LW_EXCLUSIVE);

restart:;

	/*
	 * 当我们保持锁定时，进行一个重要的安全检查：当前的终点页面
	 * 不能有资格被删除。
	 */
	if (fc_ctl->PagePrecedes(fc_shared->latest_page_number, fc_cutoffPage))
	{
		LWLockRelease(fc_shared->ControlLock);
		ereport(LOG,
				(errmsg("could not truncate directory \"%s\": apparent wraparound",
						fc_ctl->Dir)));
		return;
	}

	for (fc_slotno = 0; fc_slotno < fc_shared->num_slots; fc_slotno++)
	{
		if (fc_shared->page_status[fc_slotno] == SLRU_PAGE_EMPTY)
			continue;
		if (!fc_ctl->PagePrecedes(fc_shared->page_number[fc_slotno], fc_cutoffPage))
			continue;

		/*
		 * 如果页面是干净的，只需将状态更改为 EMPTY（预期情况）。
		 */
		if (fc_shared->page_status[fc_slotno] == SLRU_PAGE_VALID &&
			!fc_shared->page_dirty[fc_slotno])
		{
			fc_shared->page_status[fc_slotno] = SLRU_PAGE_EMPTY;
			continue;
		}

		/*
		 * 嗯，我们有（或可能有）I/O 操作作用于该页面，因此
		 * 我们必须等待它们完成，然后再重新开始。这与 
		 * SlruSelectLRUPage 中的逻辑相同。 （XXX 如果页面是脏的，
		 * 直接丢弃而不写入不是可以吗？
		 * SlruMayDeleteSegment() 使用了更严格的资格，因此最终我们可能
		 * 无法删除此页面；即使我们不删除它，我们也不必再读取它的数据。
		 * 暂时保持逻辑与之前相同。）
		 */
		if (fc_shared->page_status[fc_slotno] == SLRU_PAGE_VALID)
			fc_SlruInternalWritePage(fc_ctl, fc_slotno, NULL);
		else
			fc_SimpleLruWaitIO(fc_ctl, fc_slotno);
		goto restart;
	}

	LWLockRelease(fc_shared->ControlLock);

	/* 现在我们可以删除旧的段落 */
	(void) SlruScanDirectory(fc_ctl, fc_SlruScanDirCbDeleteCutoff, &fc_cutoffPage);
}

/*
 * 删除单个 SLRU 段。
 *
 * 注意：这不会接触 SLRU 缓冲区本身，调用者必须确保
 * 它们要么还不包含任何内容，要么已经被清理干净。
 */
static void fc_SlruInternalDeleteSegment(SlruCtl fc_ctl, int fc_segno)
{
	char		fc_path[MAXPGPATH];

	/* 忘记排队请求的 fsync 事务。 */
	if (fc_ctl->sync_handler != SYNC_HANDLER_NONE)
	{
		FileTag		fc_tag;

		INIT_SLRUFILETAG(fc_tag, fc_ctl->sync_handler, fc_segno);
		RegisterSyncRequest(&fc_tag, SYNC_FORGET_REQUEST, true);
	}

	/* 取消链接文件。 */
	SlruFileName(fc_ctl, fc_path, fc_segno);
	ereport(DEBUG2, (errmsg_internal("removing file \"%s\"", fc_path)));
	unlink(fc_path);
}

/*
 * 删除由段号标识的单个 SLRU 段。
 */
void SlruDeleteSegment(SlruCtl fc_ctl, int fc_segno)
{
	SlruShared	fc_shared = fc_ctl->shared;
	int			fc_slotno;
	bool		fc_did_write;

	/* 清除可能存在的对该段的引用。 */
	LWLockAcquire(fc_shared->ControlLock, LW_EXCLUSIVE);
restart:
	fc_did_write = false;
	for (fc_slotno = 0; fc_slotno < fc_shared->num_slots; fc_slotno++)
	{
		int			fc_pagesegno = fc_shared->page_number[fc_slotno] / SLRU_PAGES_PER_SEGMENT;

		if (fc_shared->page_status[fc_slotno] == SLRU_PAGE_EMPTY)
			continue;

		/* 不是我们要寻找的段 */
		if (fc_pagesegno != fc_segno)
			continue;

		/* 如果页面是干净的，只需将状态更改为 EMPTY（预期情况）。 */
		if (fc_shared->page_status[fc_slotno] == SLRU_PAGE_VALID &&
			!fc_shared->page_dirty[fc_slotno])
		{
			fc_shared->page_status[fc_slotno] = SLRU_PAGE_EMPTY;
			continue;
		}

		/* 与 SimpleLruTruncate() 的逻辑相同 */
		if (fc_shared->page_status[fc_slotno] == SLRU_PAGE_VALID)
			fc_SlruInternalWritePage(fc_ctl, fc_slotno, NULL);
		else
			fc_SimpleLruWaitIO(fc_ctl, fc_slotno);

		fc_did_write = true;
	}

	/*
	 * 多加小心，再次检查。IO 函数释放控制
	 * 锁，因此可能已经读取了新页面。
	 */
	if (fc_did_write)
		goto restart;

	fc_SlruInternalDeleteSegment(fc_ctl, fc_segno);

	LWLockRelease(fc_shared->ControlLock);
}

/*
 * 确定一个段是否可以删除。
 *
 * segpage是该段的第一页，cutoffPage是SLRU中包含仍然有用数据的最旧（在
 * PagePrecedes顺序中）的页面。由于每个核心PagePrecedes回调实现了“环绕”，需要检查
 * 段的第一页和最后一页：
 *
 * first<cutoff  && last<cutoff:  是
 * first<cutoff  && last>=cutoff: 否；cutoff在该段内
 * first>=cutoff && last<cutoff:  否；环绕点在该段内
 * first>=cutoff && last>=cutoff: 否；该段的每一页都太年轻
 */
static bool fc_SlruMayDeleteSegment(SlruCtl fc_ctl, int fc_segpage, int fc_cutoffPage)
{
	int			fc_seg_last_page = fc_segpage + SLRU_PAGES_PER_SEGMENT - 1;

	Assert(fc_segpage % SLRU_PAGES_PER_SEGMENT == 0);

	return (fc_ctl->PagePrecedes(fc_segpage, fc_cutoffPage) &&
			fc_ctl->PagePrecedes(fc_seg_last_page, fc_cutoffPage));
}

#ifdef USE_ASSERT_CHECKING
static void fc_SlruPagePrecedesTestOffset(SlruCtl fc_ctl, int fc_per_page, uint32 fc_offset)
{
	TransactionId fc_lhs,
				fc_rhs;
	int			fc_newestPage,
				fc_oldestPage;
	TransactionId fc_newestXact,
				fc_oldestXact;

	/*
	 * 比较具有未定义顺序的XID对（见RFC 1982），该对处于XID空间的
	 * “对立端”。TransactionIdPrecedes()将每个视为对方的前驱。
	 * 如果RHS是oldestXact，则LHS是我们必须不分配的第一个XID。
	 */
	fc_lhs = fc_per_page + fc_offset;	/* 跳过第一页以避免非正常XIDs */
	fc_rhs = fc_lhs + (1U << 31);
	Assert(TransactionIdPrecedes(fc_lhs, fc_rhs));
	Assert(TransactionIdPrecedes(fc_rhs, fc_lhs));
	Assert(!TransactionIdPrecedes(fc_lhs - 1, fc_rhs));
	Assert(TransactionIdPrecedes(fc_rhs, fc_lhs - 1));
	Assert(TransactionIdPrecedes(fc_lhs + 1, fc_rhs));
	Assert(!TransactionIdPrecedes(fc_rhs, fc_lhs + 1));
	Assert(!TransactionIdFollowsOrEquals(fc_lhs, fc_rhs));
	Assert(!TransactionIdFollowsOrEquals(fc_rhs, fc_lhs));
	Assert(!fc_ctl->PagePrecedes(fc_lhs / fc_per_page, fc_lhs / fc_per_page));
	Assert(!fc_ctl->PagePrecedes(fc_lhs / fc_per_page, fc_rhs / fc_per_page));
	Assert(!fc_ctl->PagePrecedes(fc_rhs / fc_per_page, fc_lhs / fc_per_page));
	Assert(!fc_ctl->PagePrecedes((fc_lhs - fc_per_page) / fc_per_page, fc_rhs / fc_per_page));
	Assert(fc_ctl->PagePrecedes(fc_rhs / fc_per_page, (fc_lhs - 3 * fc_per_page) / fc_per_page));
	Assert(fc_ctl->PagePrecedes(fc_rhs / fc_per_page, (fc_lhs - 2 * fc_per_page) / fc_per_page));
	Assert(fc_ctl->PagePrecedes(fc_rhs / fc_per_page, (fc_lhs - 1 * fc_per_page) / fc_per_page)
		   || (1U << 31) % fc_per_page != 0);	/* 见CommitTsPagePrecedes() */
	Assert(fc_ctl->PagePrecedes((fc_lhs + 1 * fc_per_page) / fc_per_page, fc_rhs / fc_per_page)
		   || (1U << 31) % fc_per_page != 0);
	Assert(fc_ctl->PagePrecedes((fc_lhs + 2 * fc_per_page) / fc_per_page, fc_rhs / fc_per_page));
	Assert(fc_ctl->PagePrecedes((fc_lhs + 3 * fc_per_page) / fc_per_page, fc_rhs / fc_per_page));
	Assert(!fc_ctl->PagePrecedes(fc_rhs / fc_per_page, (fc_lhs + fc_per_page) / fc_per_page));

	/*
	 * GetNewTransactionId()已经分配了可以安全使用的最后一个XID，
	 * 而该XID位于第二个段的*最后*一页。我们不能删除该段。
	 */
	fc_newestPage = 2 * SLRU_PAGES_PER_SEGMENT - 1;
	fc_newestXact = fc_newestPage * fc_per_page + fc_offset;
	Assert(fc_newestXact / fc_per_page == fc_newestPage);
	fc_oldestXact = fc_newestXact + 1;
	fc_oldestXact -= 1U << 31;
	fc_oldestPage = fc_oldestXact / fc_per_page;
	Assert(!fc_SlruMayDeleteSegment(fc_ctl,
								 (fc_newestPage -
								  fc_newestPage % SLRU_PAGES_PER_SEGMENT),
								 fc_oldestPage));

	/*
	 * GetNewTransactionId()已经分配了可以安全使用的最后一个XID，
	 * 而该XID位于第二个段的*第一页。我们不能删除该段。
	 */
	fc_newestPage = SLRU_PAGES_PER_SEGMENT;
	fc_newestXact = fc_newestPage * fc_per_page + fc_offset;
	Assert(fc_newestXact / fc_per_page == fc_newestPage);
	fc_oldestXact = fc_newestXact + 1;
	fc_oldestXact -= 1U << 31;
	fc_oldestPage = fc_oldestXact / fc_per_page;
	Assert(!fc_SlruMayDeleteSegment(fc_ctl,
								 (fc_newestPage -
								  fc_newestPage % SLRU_PAGES_PER_SEGMENT),
								 fc_oldestPage));
}

/*
 * 单元测试一个PagePrecedes函数。
 *
 * 这假设每个uint32 >= FirstNormalTransactionId都是一个有效的键。
 * 它假设每个值占用SLRU字节的连续固定大小区域。
 * （MultiXactMemberCtl将标志和XIDs分开。AsyncCtl具有
 * 可变长度条目，没有键，且没有随机访问。这些单元测试
 * 不适用于它们。）
 */
void SlruPagePrecedesUnitTests(SlruCtl fc_ctl, int fc_per_page)
{
	/* 测试一个页面的第一个、最后一个和中间条目。 */
	fc_SlruPagePrecedesTestOffset(fc_ctl, fc_per_page, 0);
	fc_SlruPagePrecedesTestOffset(fc_ctl, fc_per_page, fc_per_page / 2);
	fc_SlruPagePrecedesTestOffset(fc_ctl, fc_per_page, fc_per_page - 1);
}
#endif

/*
 * SlruScanDirectory回调
 *		如果存在任何完全在
 *		包含作为“数据”传递的页面之前的段，则该回调报告为真。
 */
bool SlruScanDirCbReportPresence(SlruCtl fc_ctl, char *fc_filename, int fc_segpage, void *fc_data)
{
	int			fc_cutoffPage = *(int *) fc_data;

	if (fc_SlruMayDeleteSegment(fc_ctl, fc_segpage, fc_cutoffPage))
		return true;			/* 找到了一个；不再迭代 */

	return false;				/* 继续进行 */
}

/*
 * SlruScanDirectory回调。
 *		该回调删除在作为“数据”传递的段之前的段。
 */
static bool fc_SlruScanDirCbDeleteCutoff(SlruCtl fc_ctl, char *fc_filename, int fc_segpage, void *fc_data)
{
	int			fc_cutoffPage = *(int *) fc_data;

	if (fc_SlruMayDeleteSegment(fc_ctl, fc_segpage, fc_cutoffPage))
		fc_SlruInternalDeleteSegment(fc_ctl, fc_segpage / SLRU_PAGES_PER_SEGMENT);

	return false;				/* 继续进行 */
}

/*
 * SlruScanDirectory回调。
 *		该回调删除所有段。
 */
bool SlruScanDirCbDeleteAll(SlruCtl fc_ctl, char *fc_filename, int fc_segpage, void *fc_data)
{
	fc_SlruInternalDeleteSegment(fc_ctl, fc_segpage / SLRU_PAGES_PER_SEGMENT);

	return false;				/* 继续进行 */
}

/*
 * 扫描SimpleLru目录并对找到的每个文件应用回调。
 *
 * 如果回调返回真，则停止扫描。回调的最后返回值
 * 将被返回。
 *
 * 回调接收以下参数：1. 要被截断的slru的SlruCtl结构；2. 正在考虑的文件名；3. 
 * 该文件第一页的页码；4. 调用者提供给我们的不透明数据的指针。
 *
 * 注意，目录扫描的顺序无法保证。
 *
 * 注意，没有应用锁定。
 */
bool SlruScanDirectory(SlruCtl fc_ctl, SlruScanCallback fc_callback, void *fc_data)
{
	bool		fc_retval = false;
	DIR		   *fc_cldir;
	struct dirent *fc_clde;
	int			fc_segno;
	int			fc_segpage;

	fc_cldir = AllocateDir(fc_ctl->Dir);
	while ((fc_clde = ReadDir(fc_cldir, fc_ctl->Dir)) != NULL)
	{
		size_t		fc_len;

		fc_len = strlen(fc_clde->d_name);

		if ((fc_len == 4 || fc_len == 5 || fc_len == 6) &&
			strspn(fc_clde->d_name, "0123456789ABCDEF") == fc_len)
		{
			fc_segno = (int) strtol(fc_clde->d_name, NULL, 16);
			fc_segpage = fc_segno * SLRU_PAGES_PER_SEGMENT;

			elog(DEBUG2, "SlruScanDirectory invoking callback on %s/%s",
				 fc_ctl->Dir, fc_clde->d_name);
			fc_retval = fc_callback(fc_ctl, fc_clde->d_name, fc_segpage, fc_data);
			if (fc_retval)
				break;
		}
	}
	FreeDir(fc_cldir);

	return fc_retval;
}

/*
 * 各个SLRU（如clog，...）必须提供一个sync.c处理函数，以便它们能提供正确的“SlruCtl”（否则我们不知道如何构建路径），但它们只是转发到这个共同的实现，该实现执行fsync。
 */
int SlruSyncFileTag(SlruCtl fc_ctl, const FileTag *fc_ftag, char *fc_path)
{
	int			fc_fd;
	int			fc_save_errno;
	int			fc_result;

	SlruFileName(fc_ctl, fc_path, fc_ftag->segno);

	fc_fd = OpenTransientFile(fc_path, O_RDWR | PG_BINARY);
	if (fc_fd < 0)
		return -1;

	pgstat_report_wait_start(WAIT_EVENT_SLRU_FLUSH_SYNC);
	fc_result = pg_fsync(fc_fd);
	pgstat_report_wait_end();
	fc_save_errno = errno;

	CloseTransientFile(fc_fd);

	errno = fc_save_errno;
	return fc_result;
}
